iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

今天的主題:除錯——我拿到一個被動過手腳的版本

今天不學新語法。我準備了一個藏了 4 個 bug 的遊戲版本,然後用我本業的方式把它們找出來。

規則:先不看程式碼,像測產品那樣操作,再用 DevTools 追原因。

先講一個分類:大聲失敗和安靜失敗

這是我這 21 天一直在遇到、但今天才正式整理的東西:

大聲失敗 安靜失敗
例子 404、SyntaxError、TypeError 功能沒生效、算錯、畫面沒更新
使用者看得到嗎 看得到(畫面壞掉、跳錯誤) 看不到,一切正常
誰會回報 所有人 幾乎沒有人
好不好查 訊息會告訴你檔名和行號 沒有任何線索

這 21 天累積下來的安靜失敗清單:字串改一個字不生效、同名函式安靜覆蓋、遮蔽留下的 undefined、監聽器掛上 undefined、資料改了畫面沒動、使用者輸入的標籤被解析、忘記清除的計時器在背景永遠跑、亂數少了 +1、物件存進 localStorage 變成 [object Object]。

九個裡面只有一個會報錯。

而今天這四個 bug 的比例也差不多:一個大聲,三個安靜。

我的測試流程

第一輪,我完全不看程式碼,只當使用者:

進行遊戲測試
  無法執行清除記錄
    出現訊息 Uncaught ReferenceError: showRecrod is not defined
  點擊再戰一次
    血量都恢復了,但點擊攻擊後沒有扣血,顯示「請不要對小怪屍體動手!」
  切換視窗後
    遊戲頁面被清除記錄了

第二輪才打開 DevTools 追原因。這個順序是刻意的:先確認「現象」,再找「原因」。如果一開始就盯著程式碼看,很容易被自己的假設帶著走,反而漏掉使用者真正會遇到的問題。

Bug 1:清除紀錄按鈕(大聲失敗)

點下去,紀錄文字沒變,主控台跳紅字:

Uncaught ReferenceError: showRecrod is not defined

函式名稱打錯了,showRecord 寫成 showRecrod。

這個最好找,因為它會自己舉手。但它有個細節值得注意:紀錄其實真的被刪掉了。因為 removeItem 那行在前面,已經執行了;錯誤發生在下一行,只是讓畫面沒更新。

所以我後來「切換視窗回來發現紀錄不見了」,並不是第三個 bug —— 那是同一個 bug 的延遲現身:資料早就沒了,只是畫面還在騙我。

追這個 bug 的時候,我發現 DevTools 有一個選項叫「在遇到未偵測到的例外狀況時暫停」。打開它之後,程式一出錯就會自動停在那一行,右邊直接顯示例外狀況的內容,不用自己去猜。

插入 day21-3.png:程式自動停在 showRecrod() 那一行

Bug 2:小怪太早變紅(安靜失敗)

這個是我最後才找到的。

打一場,慢慢打,盯著小怪的 HP 數字什麼時候變紅 —— 30 左右就變了。

但小怪滿血 60,設計上是「剩 30% 變紅」,也就是 18 才對。

原因是判斷式裡用錯了物件:

if (state.enemy.hp <= state.hero.maxHp * 0.3)
//                        ^^^^ 應該是 enemy.maxHp

hero.maxHp 是 100,所以門檻變成 30。

這是複製貼上最典型的後遺症:型別對、語法對、程式跑得很順,只是拿錯了物件。完全不會報錯。

Bug 3:破了紀錄卻沒被記住(安靜失敗,而且最難)

「最少回合紀錄」的比較方向寫反了:

if (best === null || turns > best) {   // 應該是 turns < best

變成「花更多回合才算破紀錄」。

這個 bug 我一開始完全測不出來,原因有兩層。

第一層:我需要打贏好幾場才能比較,但 Bug 4 擋住了我 —— 打完一場按「再戰一次」,遊戲就壞掉了,根本進不了第二場。

第二層更麻煩:我用 F5 繞過之後連打三場,結果是 4、4、4。

以目前的攻擊力,每場都剛好 4 回合。根本沒有變化可以觀察。 這本身也是個發現:傷害浮動 ±3 太小了,對「最少回合挑戰」這個設計來說是個問題 —— 如果每個人都是 4 回合,那還挑戰什麼?

最後我改用另一個方法:直接在 DevTools 的「應用程式」分頁,把紀錄手動改成 6。

然後打贏一場(4 回合)。正確的行為應該是紀錄更新成 4 —— 結果它還是 6。

抓到了。

這個方法本身值得記下來:不用真的製造出測試條件,直接把系統放進那個狀態就好。我昨天才學到 localStorage 可以被手動修改,今天就拿它來當測試工具。

Bug 4:再戰之後第一擊就變鞭屍(安靜失敗)

打贏 → 按「再戰一次」→ 血量回滿、按鈕變回「攻擊!」→ 按下去卻顯示「請不要對小怪屍體動手!」

原因是 reset() 忘記重置 state.isOver。

令我意外的是:點擊再戰一次完全沒有出現任何錯誤。

這句話是我測試當下寫在筆記裡的,後來想想,「意外」這個反應本身就說明了問題:我下意識期待壞掉的東西會報錯。但安靜失敗就是不會 —— 畫面顯示得好好的,狀態卻是錯的。

插入 day21-5.png:用中斷點停在重置的瞬間,右邊範圍面板看得到所有變數的當下值

追這個 bug 的時候我用了中斷點:在程式碼的行號上點一下,程式跑到那裡就會停住,右邊的「範圍」面板會列出當下每個變數的值。

比起 console.log,中斷點的好處是不用改程式碼,而且一次看到全部變數。console.log 適合「我猜是這個變數,印出來看看」;中斷點適合「我不知道是哪個變數不對」。

哪一個最難?這題有兩種答案

對我來說,最難的是 Bug 3(紀錄比較寫反)。因為以現在的攻擊力,贏的回合數都落在 4,根本看不出有問題 —— 我得先繞過另一個 bug,再自己造條件,才測得到。

但對一般玩家來說,最難的應該是 Bug 2(血量變紅時機)。

理由是:這個遊戲沒有說明文字。玩家根本不知道「應該剩 30% 才變紅」這個規則,所以不會覺得有問題。

寫下這句話的時候我愣了一下,因為它其實是一個蠻核心的道理:

一個 bug 要能被「發現」,前提是有人知道「正確的行為應該是什麼」。

沒有規格、沒有說明,使用者就算天天看到那個畫面,也永遠不會回報。他們不是沒注意到,是沒有基準可以比對。

這解釋了我工作上很多事:為什麼測試要有規格文件、為什麼「使用者沒反應」不等於「沒問題」、為什麼有些 bug 上線好幾年才被發現 —— 通常是因為某天終於有人問了一句「這裡本來應該是這樣嗎?」

一個 bug 擋住另一個 bug

Bug 4 擋住 Bug 3 這件事,是今天最真實的一段。

在真實的測試裡這很常見。我自己就遇過:一個版面的主要功能是壞的,所以掛在上面的新功能根本沒辦法測,只能等主要功能修好。

處理方式有兩種:等修好,或想辦法繞過。今天我用 F5 繞過(重新整理會重置 isOver,但 localStorage 的紀錄會保留),沒有停在那裡等。

而繞過之後才發現,原來後面還有一個 bug 在等我。如果當時就停下來回報「再戰一次壞了」,Bug 3 到今天都不會有人知道。

三個工具的分工

今天用到的除錯工具,各有適合的場合:

工具 適合什麼時候
console.log 我猜是這個變數,印出來確認
中斷點 我不知道哪裡不對,想停下來看全部
例外狀況自動暫停 有紅字,想直接跳到出錯的那一行

另外兩個我今天才知道的小工具:

console.table(陣列);    // 把資料印成表格,看清單時超好用
console.count("標記");  // 自動計數,不用自己加變數

小結

今天搞懂的事:

  • 四個 bug 裡三個不報錯 —— 「沒有紅字」不等於「沒問題」
  • 測試順序:先當使用者確認現象,再打開工具找原因
  • 大聲失敗開發者自己就會發現,安靜失敗要有人去測才找得到
  • 中斷點適合「不知道哪裡錯」,console.log 適合「我猜是這個」
  • 可以直接修改資料來造出測試條件,不用真的重現
  • 一個 bug 會擋住另一個 bug,要想辦法繞過,不然後面的永遠測不到
  • 一個 bug 要被發現,前提是有人知道正確行為是什麼 —— 沒有規格,使用者不會回報
  • 我自己也會下意識期待「壞掉的東西會報錯」,而這正是安靜失敗能存活的原因

另外意外發現一個設計問題:傷害浮動 ±3 太小,每場都是 4 回合結束。「最少回合挑戰」需要更大的變化才有意義 —— 這件事留到後面組裝時處理。

明天做第二階段回顧,然後開始把這 22 天的零件組裝成真正的作品。


上一篇
Day20|我半年前用來測 API 的那把鑰匙,其實 XSS 兩行就能偷走
下一篇
Day22|第一階段我需要一張表,第二階段我需要一張流程圖
系列文
QA 的 JavaScript 初心之路:30 天做出一個能玩的戰鬥小遊戲《勇者 vs 小怪》 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言